using System;
using System.Collections;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Serialization.Formatters;
using Colman.Rpg;
using System.Runtime.Serialization.Formatters.Binary;
using System.Xml.Serialization;
using System.IO;
using System.Text;
using System.Collections.Generic;
using Colman.Rpg.Messaging;
using System.Net;
using System.Runtime.Remoting.Lifetime;
using System.Windows.Forms;

namespace Colman.Rpg.Connection
{
    /// <summary>
    /// The end point for communication to the server.
    /// </summary>
    public class ClientEndPoint : MarshalByRefObject, IClientCallback, IServerProxy
    {
        private IServerProxy serverProxy;
        private static ClientEndPoint instance;

        [System.Diagnostics.DebuggerStepThrough]
        private ClientEndPoint(string username, IServerProxy serverProxy)
        {
            this.username = username;
            this.serverProxy = serverProxy;
        }

        /// <summary>
        /// Gets the single instance of the <see cref="ClientEndPoint"/> class.
        /// </summary>
        public static ClientEndPoint Instance
        {
            [System.Diagnostics.DebuggerStepThrough]
            get
            {
                if (instance == null)
                    throw new InvalidOperationException("Can not access the instance before initializing the class. Please call ServerProxy.Initialize first.");

                return instance;
            }
        }

        private static void InitializeClientConnection()
        {
            if (ChannelServices.GetChannel("tcp") == null)
            {
                BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider();
                provider.TypeFilterLevel = TypeFilterLevel.Full;

                IDictionary props = new Hashtable();
                props["port"] = 0;

                //create and register the channel
                TcpChannel channel = new TcpChannel(props, new BinaryClientFormatterSinkProvider(), provider);
                ChannelServices.RegisterChannel(channel, false);
            }
        }

        /// <summary>
        /// Initializes the connection to the server.
        /// </summary>
        /// <param name="server">The server's address or ip.</param>
        /// <param name="username">The name of the user.</param>
        /// <param name="hashedPassword">The password, after being hashed by MD5.</param>
        public static void Initialize(string server, string username, string hashedPassword)
        {
            InitializeClientConnection();

            IServerProxy manager = (IServerProxy)Activator.GetObject(
                typeof(IServerProxy),
                "tcp://" + server + ":12345/Rpg");

            instance = new ClientEndPoint(username, manager);

            manager.LoginUser(instance, hashedPassword);
        }

        /// <summary>
        /// Disposes of the connection to the server.
        /// </summary>
        public static void Dispose()
        {
            RemotingServices.Disconnect((MarshalByRefObject)Instance);
            ChannelServices.UnregisterChannel(ChannelServices.GetChannel("tcp"));
        }

        #region string UserName { get; } (username)
        private string username;

        /// <summary>
        /// Gets the current user's username.
        /// </summary>
        public string UserName
        {
            [System.Diagnostics.DebuggerStepThrough]
            get
            {
                return this.username;
            }
        }
        #endregion

        private delegate void Invoker<T>(T value);
        private delegate void Invoker<T, R>(T value, R value2);

        #region | IServerProxy.SendMessage(RequestMessage) |
        private int sendMessageIds = 0;
        private Dictionary<int, Invoker<RequestMessage>> sendMessageCalls = new Dictionary<int, Invoker<RequestMessage>>();

        /// <summary>
        /// Sends a message to the server.
        /// </summary>
        /// <param name="message">The request from the server.</param>
        public void SendMessage(RequestMessage message)
        {
            message.UserName = ClientEndPoint.Instance.UserName;

            int curId = sendMessageIds++;

            this.sendMessageCalls.Add(curId, new Invoker<RequestMessage>(this.serverProxy.SendMessage));
            this.sendMessageCalls[curId].BeginInvoke(message, new AsyncCallback(this.SendMessageCallback), curId);
        }

        private void SendMessageCallback(IAsyncResult result)
        {
            try
            {
                this.sendMessageCalls[(int)result.AsyncState].EndInvoke(result);
            }
            catch (Exception ex)
            {
                MessageBox.Show("An unexpected exception has occured in the application:" + Environment.NewLine + ex.Message);
            }

            this.sendMessageCalls.Remove((int)result.AsyncState);
        }
        #endregion

        #region | IServerProxy.LoginUser(IClientCallback, string) |
        private int loginUserIds = 0;
        private Dictionary<int, Invoker<IClientCallback, string>> loginUserCalls = new Dictionary<int, Invoker<IClientCallback, string>>();

        /// <summary>
        /// Logs a user into the server.
        /// </summary>
        /// <param name="clientEntry">The callback supplied by the client.</param>
        /// <param name="hashedPassword">The password to log the user in, in an MD5 hashed form.</param>
        public void LoginUser(IClientCallback clientEntry, string hashedPassword)
        {
            int curId = loginUserIds++;

            this.loginUserCalls.Add(curId, new Invoker<IClientCallback, string>(this.serverProxy.LoginUser));
            this.loginUserCalls[curId].BeginInvoke(clientEntry, hashedPassword, new AsyncCallback(this.LoginUserCallback), curId);
        }

        private void LoginUserCallback(IAsyncResult result)
        {
            try
            {
                this.loginUserCalls[(int)result.AsyncState].EndInvoke(result);
            }
            catch (Exception ex)
            {
                MessageBox.Show("An unexpected exception has occured in the application:" + Environment.NewLine + ex.Message);
            }

            this.loginUserCalls.Remove((int)result.AsyncState);
        }
        #endregion

        #region | IServerProxy.LogOffUser(string) |
        private int logOffUserIds = 0;
        private Dictionary<int, Invoker<string>> logOffUserCalls = new Dictionary<int, Invoker<string>>();

        /// <summary>
        /// Logs a user out of the server.
        /// </summary>
        /// <param name="username">The user's name.</param>
        public void LogOffUser(string username)
        {
            int curId = logOffUserIds++;

            this.logOffUserCalls.Add(curId, new Invoker<string>(this.serverProxy.LogOffUser));
            this.logOffUserCalls[curId].BeginInvoke(username, new AsyncCallback(this.LogOffUserCallback), curId);
        }

        private void LogOffUserCallback(IAsyncResult result)
        {
            try
            {
                this.logOffUserCalls[(int)result.AsyncState].EndInvoke(result);
            }
            catch (Exception)
            {
                // Do nothing, since we don't care.
                System.Diagnostics.Debugger.Break();
            }

            this.logOffUserCalls.Remove((int)result.AsyncState);
        }
        #endregion

        #region | IClientCallback.SendMessage(ResponseMessage) |
        private int getMessageIds = 0;
        private Dictionary<int, Invoker<MessageReceivedEventArgs>> getMessageCalls = new Dictionary<int, Invoker<MessageReceivedEventArgs>>();

        void IClientCallback.SendMessage(ResponseMessage message)
        {
            int curId = getMessageIds++;

            this.getMessageCalls.Add(curId, new Invoker<MessageReceivedEventArgs>(this.OnMessageReceived));
            this.getMessageCalls[curId].BeginInvoke(new MessageReceivedEventArgs(message), new AsyncCallback(this.GetMessageCallback), curId);
        }

        private void GetMessageCallback(IAsyncResult result)
        {
            try
            {
                this.getMessageCalls[(int)result.AsyncState].EndInvoke(result);
            }
            catch (Exception ex)
            {
                MessageBox.Show("An unexpected exception has occured in the application:" + Environment.NewLine + ex.Message);
            }

            this.getMessageCalls.Remove((int)result.AsyncState);
        }
        #endregion

        #region event EventHandler<MessageReceivedEventArgs> MessageReceived
        /// <summary>
        /// Invokes the <see cref="MessageReceived" /> event.
        /// </summary>
        /// <param name="e">The event's arguments.</param>
        [System.Diagnostics.DebuggerStepThrough]
        protected virtual void OnMessageReceived(MessageReceivedEventArgs e)
        {
            if (this.MessageReceived != null)
                this.MessageReceived(this, e);
        }

        /// <summary>
        /// Raised when a message is received from the server.
        /// </summary>
        public event EventHandler<MessageReceivedEventArgs> MessageReceived;
        #endregion

        /// <summary>
        /// Obtains a lifetime service object to control the lifetime policy for thisinstance.
        /// </summary>
        /// <returns>
        /// An object of type System.Runtime.Remoting.Lifetime.ILease used to control 
        /// the lifetime policy for this instance. This is the current lifetime service
        /// object for this instance if one exists;
        /// otherwise, a new lifetime service object initialized to the value of the 
        /// <see cref="System.Runtime.Remoting.Lifetime.LifetimeServices.LeaseManagerPollTime"/> property.
        /// </returns>
        public override object InitializeLifetimeService()
        {
            // Let me live for as long as I need to.
            ILease lease = (ILease)base.InitializeLifetimeService();

            if (lease.CurrentState == LeaseState.Initial)
            {
                lease.InitialLeaseTime = TimeSpan.Zero;
            }

            return lease;
        }
    }

    /// <summary>
    /// Used in the <see cref="ClientEndPoint.MessageReceived"/> event.
    /// </summary>
    public class MessageReceivedEventArgs : EventArgs
    {
        /// <summary>
        /// Creates a new instance of <see cref="MessageReceivedEventArgs" />.
        /// </summary>
        /// <param name="message">Represents a message sent from the server.</param>
        [System.Diagnostics.DebuggerStepThrough]
        public MessageReceivedEventArgs(ResponseMessage message)
        {
            this.message = message;
        }

        #region ResponseMessage Message { get; } (message)
        private ResponseMessage message;

        /// <summary>
        /// Gets a message sent from the server.
        /// </summary>
        public ResponseMessage Message
        {
            [System.Diagnostics.DebuggerStepThrough]
            get
            {
                return this.message;
            }
        }
        #endregion
    }
}
